-
Notifications
You must be signed in to change notification settings - Fork 50
[7팀 이현지] Chapter2-1. 프레임워크 없이 SPA 만들기 #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
test: 로컬과는 달라서 셋업 시나리오 수정
JunilHwang
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 피드백은 n8n + ai (gpt-5-mini)를 활용하여 자동으로 생성된 내용입니다.
전체 리뷰 요약
이번 제출물은 SPA 형태의 쇼핑몰을 vanilla JS 환경에서 자체 라우터, 컴포넌트 시스템, 상태관리, 그리고 로컬 스토리지 기반 장바구니 기능까지 충실히 구현한 점이 인상적입니다. 특히 토스트, 무한 스크롤, 상세 페이지 및 카테고리 필터링을 포함한 전반적인 요구사항을 포괄적으로 다루고 있습니다.
주요 설계 포인트 및 개선 제안
-
상태 관리 및 렌더링 효율성: 컴포넌트 내부에 단순 innerHTML 덮어쓰기 방식이 많아 복잡한 상태 변화 발생 시 불필요한 리렌더링이 빈번할 수 있습니다. 부분 렌더링 또는 가상 DOM, 혹은 상태관리 훅 도입이 확장성 및 유지보수에 큰 도움이 됩니다.
-
무한 스크롤 및 데이터 로딩 로직 분리: 현재 HomePage 내 무한 스크롤 로직이 내부 상태와 렌더링을 모두 담당하여 복잡도가 높고, 에러 처리 및 사용자 경험 개선에 제약이 있습니다.
- 추후에는 로딩 상태와 에러 상태를 구분하여 UI에서 명확히 사용자에게 알려주고, 재시도 기능도 고려해보세요.
-
라우터 구현 및 URL관리: 자체적인 경로 매칭 및 쿼리 파라미터 관리가 잘 되어 있으나 라우팅 경로가 복잡해질 때를 대비하여 표준 라이브러리(path-to-regexp 등) 도입을 검토할 수 있습니다.
-
장바구니 및 로컬스토리지 관리: 잘 구현되어 있으나 상태 변화 감지 이벤트 및 데이터 파싱에서 안정성(타입체킹, 예외 처리 등)을 강화한다면 더 견고해집니다.
-
컴포넌트 이벤트 핸들링: 이벤트 위임 및 이벤트 핸들러 분리로 인해 유지보수가 비교적 쉬운 편이나, 컴포넌트 내부 이벤트 분리 및 재활용 가능한 유틸 함수로 리팩토링 가능성이 있습니다.
-
토스트 메시지 관리: 토스트 단일 인스턴스 관리가 쉬우나 다중 토스트 큐 운영과 race condition 방지, 메시지 우선순위 처리 등을 검토해 확장성을 높일 수 있습니다.
전반적으로 요구 사항을 충실하게 구현하여 기본기와 문제 해결 능력이 뛰어납니다. 다만 앞으로 확장성과 유지보수를 위해 상태 관리, 컴포넌트 분리, 이벤트 및 렌더링 최적화에 더 집중해보시길 권장합니다.
추가 요구사항 예시와 개선
구체적 문제 상황: 무한 스크롤에 에러가 발생해도 사용자가 알 수 없고, 재시도할 방법이 없음
- 현재는 무한 스크롤에서 로딩 실패시 UI 반영이나 에러 메세지 표시가 없어 사용자 경험 저하 우려
현재 코드 한계
- 에러 발생 시 로딩 인디케이터가 사라지지 않거나 오류 상태가 무시됨
- 재시도를 위한 버튼 UI가 없음
근본 이유
- 로딩 상태(isLoading)와 에러 상태(error)가 별도로 존재하지 않고, loadNextPage 내부에서 try/catch 누락
개선 방안
- 에러 상태를 관리하는 상태 변수를 추가하여 UI에 에러 메시지와 재시도 버튼 표시
- 재시도 시 동일 함수 호출 가능하도록 로직 재구성
- IntersectionObserver 내 구간 진입 시 무조건 재시도 유도
// 개선 예시
async loadNextPage() {
if (this.isLoading) return;
this.isLoading = true;
this.error = null;
try {
// fetch data
} catch (err) {
this.error = err.message || "로드 실패";
}
this.isLoading = false;
this.render();
}
// 에러 UI 추가
if(this.error) {
return `<div>
<p>에러가 발생했습니다: ${this.error}</p>
<button onclick="loadNextPage()">재시도</button>
</div>`;
}로컬에서는 테스트가 통과하는데 GitHub에서는 실패하는 경우에 대한 답변
-
환경 차이 확인:
- 로컬 환경과 GitHub CI 환경 간의 Node.js 버전, 브라우저 혹은 Web Test Runner 설정 차이로 인한 동작 차이가 발생할 수 있습니다.
- 테스트 실행 환경에서 네트워크 지연, 테스트 타임아웃 설정, 동기/비동기 처리 타이밍 문제도 원인일 수 있습니다.
-
비동기 및 타이밍 문제:
- E2E 테스트에서 비동기 데이터 로딩 및 인터렉션이 로컬과 CI에서 차이가 생길 수 있습니다.
- 예를 들어 CI는 더 느려서 로딩 조건이 정상 작동 안 하거나, 타임아웃 설정이 충분하지 않을 수 있습니다.
-
테스트 코드 및 데이터 의존성:
- 테스트가 외부 데이터 변경 혹은 Mock 데이터 변경에 의존하고 있는지 확인해 보세요.
- Mock 서버 혹은 핸들러 설정이 CI에서 올바르게 동작하는지 점검 필요합니다.
-
의존성 및 빌드 차이:
- 로컬 의존성과 빌드 캐시가 최신인지,
- GitHub 워크플로에서 캐시 문제나 버전 불일치 없는지 확인해보면 좋습니다.
-
권장 해결책:
- CI 환경에서의 테스트 로그 및 리포트를 상세히 확인하여 실패 원인에 대한 단서를 찾습니다.
- 타임아웃이나 기다림(wait) 시간을 좀 더 넉넉하게 조정해봅니다.
- Mock 서버 및 API 핸들링이 CI 환경에서도 제대로 초기화되고 있는지 재검토합니다.
- 가능하면 로컬과 CI에서 동일한 브라우저 버전 및 테스트 환경을 맞추어 동기화를 시도합니다.
-
추가 공부 방법 조언:
- 실패하는 테스트 코드의 로그, 스택트레이스를 바탕으로 문제 영역을 구체적으로 좁히는 연습
- 비동기 코드 테스트 전략 및 플레이라이트/테스트 러너 문서 반복 학습
- Mocking과 가짜 데이터 관리의 중요성 학습
테스트는 환경에 따라 미묘한 차이가 있을 수 있으며 완벽하게 통과하기 위해선 환경 설정과 코드를 꾸준히 조율하는 과정이 필요합니다. 차분히 log를 분석하며 개선해보세요. 의문점 남으면 언제든 추가 질문 주세요!
| } | ||
| } | ||
|
|
||
| updateProductList() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. 문제상황 제시
현재 페이지네이션을 무한 스크롤 형태로 구현했는데, 이 로직이 다소 복잡하고 React/Vue같은 프레임워크 없이 수동으로 상태와 UI를 관리 중입니다.
- [한계점 1] 무한 스크롤이 별도의 상태(isLoading, observer)를 직접 컨트롤하면서 구현돼 관리가 어렵고 유지보수가 까다로움
- [한계점 2] 다음 페이지 데이터 병합 후 수동으로 전체 목록 재렌더링 처리, 이로 인해 성능 저하 혹은 UI 비일관성 가능성 존재
- [한계점 3] 혹여나 로딩 중 오류 발생 시 에러 핸들링이 부족해 사용자 경험에 부정적 영향
2. 근본 원인
무한 스크롤 페이지네이션과 데이터 상태 관리 로직이 컴포넌트 내 여러 곳에 흩어져 있고 단일 책임 원칙에 맞지 않아 복잡도가 높음
- 상태변경, 데이터패치, UI업데이트가 느슨하게 연계되어 있어 확장 또는 테스트에 취약
3. 개선 구조
현재 구조:
- HomePage 컴포넌트 내 isLoading, observer 상태를 직접 선언 및 관리
- loadNextPage 함수에서 fetch, 데이터 병합, UI 업데이트를 모두 수행
- IntersectionObserver 로딩 트리거 직접 핸들링
개선된 구조:
- 상태 관리와 데이터 로딩 로직 분리
- API 호출과 UI 갱신 단계를 명확히 분리하고, 별도의 데이터 스토어 혹은 상태 관리 훅 도입 고려
- 로딩, 에러, 데이터 상태를 통합하는 상태 머신 도입하거나 별도 Hook 함수 관리
개선 사항:
- 데이터 로딩 상태(isLoading, error 등)를 useState 또는 이와 유사한 상태 관리 기법으로 명확히 관리
- 데이터 추가 로딩 시 에러 처리 및 재시도 UI 제공
- IntersectionObserver 콜백에서 부하를 줄이기 위해 debounce나 throttle 적용 검토
// ❌ 현재 방식
async loadNextPage() {
if (this.props.isPending || this.isLoading) return;
this.isLoading = true;
// ... fetch, 병합 후 직접 DOM 업데이트
this.isLoading = false;
}
// ✅ 개선 방식
async loadNextPage() {
try {
this.setState({ isLoading: true });
// API 호출
// 상태 업데이트 (loaderData에 새 상품 추가 등)
this.setState({ isLoading: false, error: null });
} catch(error) {
this.setState({ isLoading: false, error });
}
}
// UI 렌더링구조에서 isLoading 및 error 상태에 따른 렌더링 적용|
|
||
| export class CartUtil { | ||
| static addCard(product, count = 1) { | ||
| const existCartItems = JSON.parse(LocalStorageUtil.getItem("shopping_cart") ?? "{}")?.items ?? []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. 문제상황 제시
장바구니에 이미 존재하는 상품을 추가할 때 quantity 증가 로직의 안전성 문제가 있습니다.
- [한계점 1] 존재하는 아이템이 있을 경우 quantity 증가 처리 시 존재하는 값이 숫자가 아닐 경우 오류 발생 가능
- [한계점 2] 숫자 변환 없이 바로 연산, 타입 안정성 미흡
2. 근본 원인
타입 검증 없이 quantity를 직접 증가시키고, 로컬 스토리지에 객체를 재설정하기 때문에 무결성을 강제하지 않음
3. 개선 구조
- 제품 추가 시 quantity 값을 항상 숫자 타입으로 안전하게 변환 후 연산
- LocalStorageUtil 안에서도 데이터 구조에 대한 기본 검증 추가
// ❌ 현재
if (existCartItem) {
existCartItem.quantity = existCartItem.quantity + count;
}
// ✅ 개선
if (existCartItem) {
existCartItem.quantity = Number(existCartItem.quantity || 0) + Number(count);
}이렇게 하면 예상치 못한 수량 관련 오류를 예방할 수 있습니다.
| </label> | ||
| <!-- 상품 이미지 --> | ||
| <div class="w-16 h-16 bg-gray-100 rounded-lg overflow-hidden mr-3 flex-shrink-0"> | ||
| <img |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. 문제상황 제시
현재 CartModal은 전부 문자열 템플릿으로 구성되어 복잡하고, 관리가 어렵습니다.
- [한계점 1] 상태 변화에 따른 부분적 렌더링이 불가해 전체 재렌더링만 가능해 비효율적
- [한계점 2] 이벤트를 일괄 바인딩하지 않고 상위 컨테이너에서 위임하는 패턴 미흡
2. 근본 원인
동적 상태(선택, 수량 등)를 모두 문자열 템플릿 방식으로 처리하여 UI 업데이트 및 이벤트 연결이 비효율적임
3. 개선 구조
- 컴포넌트 내부에 하위 컴포넌트(Entity)를 클래스로 분리하거나 가벼운 상태 관리 추가
- 이벤트 위임 구문을 별도의 유틸로 관리하여 가독성 개선
- 상태 변화별 부분 업데이트 혹은 React/Vue 같은 프레임워크 도입 고려
|
|
||
| static setItem(key, value) { | ||
| localStorage.setItem(key, value); | ||
| window.dispatchEvent( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. 문제상황 제시
로컬스토리지 변경 이벤트를 전역 커스텀 이벤트로 방출하여 여러 컴포넌트가 구독할 수 있게 구현했는데, "localStorageChange" 이벤트명이 일반적이지 않고, 중복 발생 시 문제 발생 가능
- [한계점 1] 표준 로컬스토리지 이벤트명(
storage)를 사용하지 않고 별도의 이벤트를 따로 방출 - [한계점 2] 각 컴포넌트가 별도 EventListener를 등록해야 하므로 번거로움과 성능 저하 가능성
2. 근본 원인
이벤트명을 커스텀화하면서 브라우저 기본 이벤트와 혼동 발생 가능하며, cross-tab 동기화는 따로 처리하지 않고 있음
3. 개선 구조
- 기본 브라우저의
window.addEventListener('storage', callback)활용 권장 - 이와 더불어 커스텀 이벤트는 명확한 네임스페이스를 갖도록 개선
- 싱글톤 이벤트 버스 혹은 Pub/Sub 패턴 도입 고려
| unmount() { | ||
| this.$container.innerHTML = ""; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. 문제상황 제시
컴포넌트 기본 클래스를 만들어 재사용하도록 했으나, 렌더링 로직이 단순 innerHTML 덮어쓰기여서 부분 업데이트가 항상 전체 재렌더링을 일으킴
- [한계점 1] 전체 innerHTML 재설정으로 인한 성능 저하
- [한계점 2] 상태 변화가 많아질 때 불필요한 DOM 업데이트 다량 발생
- [한계점 3] 이벤트 바인딩과 같은 DOM 참조가 초기화될 위험
2. 근본 원인
실제 DOM 디프 연산을 하지 않고 무조건 innerHTML로 덮어쓰기 때문
3. 개선 구조
- 가상 DOM이나 템플릿 엔진 사용 고려
- 상태 변경 시 변경된 부분만 업데이트하는 메서드 추가
- 이벤트 바인딩 관리 방식을 개선하여 리렌더링 시 문제 방지
| this.props.loaderData.pagination = nextPageData.pagination; | ||
|
|
||
| // 상품 목록만 다시 렌더링 | ||
| this.updateProductList(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. 문제상황 제시
검색어 입력 엔터 처리 로직과 쿼리스트링 관리가 여러 곳에 흩어져 있어 중복과 혼란 소지 있음
- [한계점 1] 쿼리스트링 관련 유틸이 복수 존재하여 관리가 복잡함
- [한계점 2] 입력값이 빈 문자열일 때 처리 로직이 분산됨
- [한계점 3] 카테고리나 검색어 변경 시 불필요한 새로고침 가능성 내포
2. 근본 원인
쿼리스트링 유틸과 이벤트 핸들러가 일관적인 방식으로 통합되어 있지 않음
3. 개선 구조
- 쿼리스트링 조작 로직을 통합 유틸 함수로 분리해 재사용성 향상
- 입력값에 따라 navigateTo 호출 로직으로 단일화
- URL 변경 전 최소한의 검증 수행
| try { | ||
| this.current.params = matched.params; | ||
| this.current.queryString = matched.queryString; | ||
| this.current.loaderData = await matched.loader({ params: matched.params, queryString: matched.queryString }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. 문제상황 제시
라우터에서 경로 매칭 시 정규 표현식으로 간단히 처리하는데, 파라미터 경로에 하위 또는 쿼리 파라미터 복잡도가 증가하면 확장성 한계 발생
- [한계점 1] 복잡한 경로나 선택적 파라미터, 와일드카드 지원 부족
- [한계점 2] 매치 실패 시 NotFound 처리 외의 다중 에러 처리 어려움
- [한계점 3] queryString과 params 비교 로직이 단순 JSON stringify 비교로 최적화 안됨
2. 근본 원인
프레임워크 없이 직접 정규표현식으로 경로 처리와 쿼리 파라미터 관리를 모두 처리하기 때문
3. 개선 구조
- path-to-regexp 같은 라이브러리 도입으로 경로 매칭과 파라미터 추출 효율화
- 쿼리스트링은 URLSearchParams 분리 관리 및 변경 감지 개선
- 매칭 결과 캐싱 등을 통한 성능 최적화 검토
| existingToast.remove(); | ||
| } | ||
|
|
||
| // 토스트 컨테이너 생성 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. 문제상황 제시
토스트 메시지 중복 생성 시 기존 토스트를 제거하는 처리 방식은 좋으나, 동시에 여러 토스트 메시지 처리가 어렵고, 시간차에 의한 race condition 가능성 있음
- [한계점 1] 여러 토스트가 중복 발생해도 오직 하나만 보여짐
- [한계점 2] 비동기 상황에서 hide가 의도하지 않은 토스트를 제거할 수 있음
- [한계점 3] 전역 싱글톤 토스트 컨트롤러 불분명
2. 근본 원인
DOM요소를 직접 생성/삭제 방식으로 토스트 단일 인스턴스만 관리하기 때문
3. 개선 구조
- 토스트 큐(Queue) 관리 시스템 도입
- 토스트 컴포넌트 상태 관리 객체로 분리
- show/hide 타이밍 컨트롤러 별도 분리 및 안전성 강화
| const cache = {}; | ||
|
|
||
| // 상품 목록 조회 | ||
| export async function getProducts(params = {}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. 문제상황 제시
상품 상세 조회 시 캐싱을 위해 전역 객체를 활용하였으나,
- [한계점 1] 전역 캐시 객체가 무한히 커질 위험
- [한계점 2] 상품 정보 변경을 반영할 수 없는 정적 캐시
- [한계점 3] 동시성 제어(캐시 초기화 중복 호출) 미지원
2. 근본 원인
간단히 메모리 캐시에만 의존해 버전 관리, 만료 정책 등의 부재
3. 개선 구조
- LRU 캐시 또는 TTL 기반 캐시 정책 도입
- 변경 감지 혹은 ETag 방식 등을 활용한 서버와의 캐시 동기화
- Promise 캐싱 및 중복 요청 방지 처리
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test
devchaeyoung
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 피드백은 n8n + ai (gpt-5-mini)를 활용하여 자동으로 생성된 내용입니다.
현지님 이번 PR에서는 SPA 라우터 설계와 컴포넌트 구조 설계를 잘 시도하신 점이 돋보입니다👍. 특히 Router가 path, loader, component를 체계적으로 처리하며 공통된 props 인터페이스를 적용하신 점, 장바구니 기능을 명확히 모듈화하고 로컬스토리지 연동을 진행한 점이 인상적입니다. 또한 무한 스크롤, 상품 조회 캐시 등 요구사항을 충실히 반영하셨습니다.
추가 질문에서 현지님은 라우터 loader의 중복 호출 문제와 데이터 캐싱 방안에 대해 고민하셨는데, 이는 실제 오픈소스도 복잡하고 많은 고민을 하는 주제입니다. 현재 구조에서 loader는 라우팅 시마다 무조건 호출되는 구조라 캐시가 어렵습니다. 실무에서는 상태관리 라이브러리나 전역 스토어, 요청 캐시 라이브러리를 도입해 이 문제를 해결합니다.
기능적 완성도는 훌륭하지만, 장바구니 상태 변경에 따른 렌더링 이슈(깜빡임)는 렌더링 중심 구조가 아닌 상태 변경 알림 기반 구조를 고민해보시면 좋겠습니다. 전반적으로 관심사 분리가 비교적 잘 되어 있으나, 이벤트 핸들러와 렌더링 로직이 한 파일에 몰려있는 부분은 향후 모듈화를 더 진행할 수도 있겠습니다.
질문에대한 답변
1. 질문 요약
현지님께서는 셀프 회고에서 SPA 라우터 구현 시 loader가 URL 변화마다 계속 호출되어 중복 API 요청이 발생하는 문제와 이에 대한 캐싱 방안, 그리고 장바구니 화면 렌더링 시 깜빡임 현상에 대해 고민하셨습니다.
2. 현재 선택의 장단점
- loader를 통해 데이터를 불러오는 설계는 데이터 의존성을 분리하는 좋은 시도입니다.
- 그러나 라우터가 매번 URL 변경마다 loader를 무조건 호출하며, 이미 받아온 데이터를 재활용하지 못합니다.
- 로컬 상태나 캐시 없이 매번 리로딩하다 보니 불필요한 네트워크 비용과 UX 저하가 발생할 수 있습니다.
- 장바구니는 명령형으로 바로 상태 변경하여 렌더를 피하는 방식이라 깜빡임을 줄였지만, 명확한 상태-뷰 동기화 로직이 없어 유지보수가 어려워질 수 있습니다.
3. 실무에서라면 이렇게 설계할 것 같아요
- 상태관리 도입 : 전역 스토어(Redux, Vuex, MobX 등)를 도입하거나, 최소한 CacheStore 모듈을 만들어 API 응답을 캐시합니다. 라우터 loader는 캐시를 먼저 조회하고 없을 때만 요청하도록 하여 중복 호출을 막습니다.
// 간단 캐시 예시
const productCache = new Map();
async function getProductCached(id) {
if (productCache.has(id)) {
return productCache.get(id);
}
const data = await fetch(`/api/products/${id}`).then(res => res.json());
productCache.set(id, data);
return data;
}- 상태-뷰 업데이트 패턴 : 상태가 바뀔 때마다 자동으로 필요한 부분만 갱신하는 옵저버 패턴 또는 리액티브 데이터 바인딩을 도입합니다.
- 컴포넌트 재렌더링 최소화 : 전체 페이지 render()가 아닌, 상태 변화가 일어난 컴포넌트만 부분 갱신하는 전략을 구현합니다.
4. 앞으로 구조를 잡을 때 참고하면 좋은 포인트
- SPA 내 데이터 로딩과 캐싱 전략을 명확히 구분하세요. 라우터는 '단순 URL 매칭 + 상태 전환' 역할만 담당하게 하고, 실제 데이터 상태 관리는 스토어가 맡게 하세요.
- loader 재호출 제어는 캐시나 stale-while-revalidate 전략으로 개선 가능합니다.
- 이벤트 핸들러는 관심사별로 분리하고, 상태와 UI 업데이트 로직은 최대한 분리하는 것이 유지보수와 테스트에 유리합니다.
- 실제 React-router, Vue-router 소스 코드와 사용하는 스테이트 관리 라이브러리들 구현 방식을 참고하면 도움이 될 겁니다.
현지님, 이번 과제에서 세운 훌륭한 토대 위에 상태 관리와 캐싱 패턴을 조금씩 확장해 보시면 SPA 아키텍처에 대한 이해가 더욱 깊어질 거예요. 앞으로도 차근차근 좋은 구조 만들어 가시길 응원합니다! 👍
| loader: async ({ params }) => { | ||
| const productId = params.productId; | ||
| const product = await getProduct(productId); | ||
| let relatedProducts = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현지님, 메인 진입점에서 라우터 초기화와 페이지 렌더링을 깔끔하게 구성하셨어요👍. 라우터마다 loader를 통해 데이터를 비동기로 받아와 렌더링하는 점도 잘 설계하셨고, 어플리케이션 진입 시 모달과 이벤트 리스너를 명확히 분리한 것도 좋아보입니다.
다만 장바구니 상태 변경에 대해 렌더링을 강제로 트리거하지 않고 명령형으로 처리하는 부분에서 깜박임이 완화되었지만, 장기적으로는 상태 관리 도구나 옵저버 패턴 도입을 고민해보면 좋을 것 같아요. 모든 상태 업데이트를 render 호출 없이 처리하면 특정 상태에서 UI와 데이터가 불일치할 위험이 있으니까요.
(예를 들어 React나 Vue처럼 상태 관리 후에 필요한 부분만 재렌더링하는 방식과 유사한 구조입니다.)
| } | ||
| return { component: NotFoundPage, loader: () => Promise.resolve({}), params: {}, queryString: {} }; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
라우터에서 path 파라미터와 쿼리스트링을 모두 추출하고, loader와 컴포넌트 바인딩을 잘 구현하신 점이 구조적으로 매우 훌륭해요👍. 컴포넌트에 공통 props를 전달하도록 만든 것도 확장성 측면에서 적절합니다.
다만 loader가 URL 체인지마다 호출되면서 API 중복 호출이 발생하는 점은 현실에서도 흔한 고민입니다. 이를 해결하려면 캐싱 전략이나 조건부 호출 로직이 필요하며, 데이터 캐싱과 fetch 재사용을 위해 별도의 상태관리 레이어(스토어) 혹은 요청 결과 캐싱 라이브러리를 도입하는 것도 좋은 방법입니다.
또는 로드 완료된 상태인지 플래그를 두고, 필요 시에만 loader를 호출하는 로직으로 개선할 수 있어요.
| @@ -0,0 +1,107 @@ | |||
| import { ToastManager } from "../../../../항해99/front_7th_chapter2-1/src/utils/toast"; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
장바구니 관련 로컬스토리지 읽기/쓰기, 상태 업데이트, 토스트 알림 기능을 한 곳에 잘 모으셨네요👍 상태가 기능별 쪼개기로 명확하게 역할 구분되어 있고, 이벤트 발생 시 로컬스토리지 변화를 커스텀 이벤트로 알리는 점도 인상적입니다.
한 가지 제안드리자면, 매번 JSON.parse/JSON.stringify를 사용하는 대신, 내부적으로 로컬스토리지와 JS 객체 상태를 싱크시키는 구조로 조금 더 추상화해보시면 사용이 더 편리하고 안전할 것 같아요. 현재 코드에서는 item이 없다면 기본값을 처리하는 부분이 매 호출마다 반복됩니다.
(예: Singleton 형태의 Cart 상태체크 및 변경 메서드로 묶으면 실수를 줄일 수 있음)
| </label> | ||
| <!-- 상품 이미지 --> | ||
| <div class="w-16 h-16 bg-gray-100 rounded-lg overflow-hidden mr-3 flex-shrink-0"> | ||
| <img |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
장바구니 모달 컴포넌트가 카트 유틸 메서드에서 받아온 데이터를 기반으로 동적으로 잘 렌더링되고 있습니다👍.
상품 별 수량 변경, 선택, 삭제, 전체 선택 등의 UI가 잘 갖춰져 있고, 로직과 UI가 적절히 분리되어 있어 기능 확장 시 용이한 구조예요.
다만 현재 장바구니 데이터 변경 시 전체 모달을 다시 렌더링하는 방식인데, UI 일부만 갱신시키거나, 상태 변경 관리를 통해 변화된 부분만 효율적으로 처리할 방법도 생각해보시면 좋겠습니다.
또한 접근성 측면에서 checkbox 및 버튼에 대한 keyboard/스크린리더 대응이 어떻게 되어 있는지도 확인해보면 좋을 것 같아요.
|
|
||
| export const CartButton = () => { | ||
| const cartCount = CartUtil.getCartItems().length; | ||
| return `<button |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
헤더 컴포넌트가 현재 경로에 따라 상세 페이지 여부 판단 및 장바구니 아이콘에 구매 아이템 수 표시까지 잘 처리하고 있습니다.
상태와 UI를 분리해 CartUtil의 상태와 UI 렌더링을 연결하는 형태가 깔끔해요👍.
다만, 카트 아이템 수가 변경될 때마다 Header를 어떻게 갱신할지 고민이 될 수 있는데, 현재 main.js에서 updateCartCount를 호출하는 구조도 참고하면 좋겠습니다.
| this.updateProductList(); | ||
|
|
||
| // URL 업데이트 (current 파라미터 추가/업데이트) | ||
| params.set("current", nextPage.toString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HomePage 클래스를 통해 검색, 카테고리 필터, 정렬, 페이지당 상품 수 변경, 무한 스크롤 페이징을 완성도 있게 구현하신 점👍
IntersectionObserver를 사용해 무한 스크롤을 처리한 점이 요구사항에 잘 부합합니다.
다만, 현재 loadNextPage 내부에서 props의 loaderData를 직접 조작해서 상품 목록을 업데이트하는 방식은 향후 상태관리 패턴을 도입하면 더욱 명료해질 것 같아요. 컴포넌트 상태가 외부에서 직접 변경되는 시나리오는 추적이 어렵고 버그의 원인이 될 수 있으니, 상태 변경 메서드나 action dispatcher를 고민해보시길 추천드립니다.
그리고 페이지나 필터 조건이 복잡해지고 다수의 컴포넌트가 연동된다면, 전역 상태 관리나 컨텍스트 패턴을 검토해도 좋습니다.
| const currentValue = Number($quantity.value); | ||
| const maxValue = Number($quantity.max); | ||
| if (currentValue < maxValue) { | ||
| $quantity.value = currentValue + 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DetailPage 컴포넌트가 클래스로 구현되어 있고 마운트/언마운트 시 이벤트 바인딩 및 해제를 명확히 하셨네요👍
상품 상세 정보, 수량 증가/감소, 장바구니 추가, 관련 상품 클릭 등 UI 이벤트 처리가 깔끔합니다.
커스텀 컴포넌트 구조를 활용한 재사용성이 기대되며 PageLayout과 잘 연동되어 있습니다.
다만 로드 중 UI 처리는 단순 스피너로 처리하고 있는데, 실제 에러 상태 대응(예: 상세 실패)까지 고려하면 더 좋을 것 같아요.
| @@ -1,3 +1,5 @@ | |||
| const cache = {}; | |||
|
|
|||
| // 상품 목록 조회 | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API 호출 함수들에 간단한 캐싱을 적용한 점이 인상적입니다👍
특히 상품 상세 조회 시 캐시를 활용해서 중복 호출을 최소화한 것은 실무에서도 흔히 사용하는 최적화 기법 중 하나예요.
다만 캐시 키 관리, 캐시 만료 정책 등을 추가로 고려하면 데이터 신선도 문제를 완화할 수 있습니다.
또한, 네트워크 오류 시 폴백 처리(재시도 로직 등)도 향후 보완해보시면 좋겠습니다.
| @@ -0,0 +1,46 @@ | |||
| export const getQueryStringExcluding = (keyToExclude) => { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
URLSearchParams를 기반으로 쿼리스트링을 관리하는 유틸을 잘 만드셨어요👍.
특히 제외, 추가, 값 조회 함수가 명확하게 분리되어 있어서 유지보수와 확장이 용이해 보입니다.
이러한 헬퍼 함수들은 SPA URL 상태 관리에서 편리하고 오류를 줄여 줍니다. 향후 더 복잡한 쿼리 조작이 필요할 때도 활용도가 높을 것 같아요.
한 가지 팁은 단위 테스트를 작성하시면 신뢰성 확보에 도움이 됩니다.
| ${_404_} | ||
| `; | ||
| } | ||
| // 새로운 HTML로 덮어쓰기 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
장바구니 모달 오픈/클로즈, 수량조절, 선택, 삭제 등 주요 UI 이벤트를 바디에 위임하는 이벤트 위임 패턴으로 관리한 점이 유지보수에 좋은 구조예요👍
하지만 이벤트 핸들러가 한 파일에 집중되어 있어, 모듈 분리 및 리팩토링으로 관심사 분리를 한 번 시도해보면 좋겠습니다.
예를 들어, CartModal 컴포넌트 내부에서 그 행위들을 메서드로 분리하거나, 별도 컨트롤러 클래스를 만들어 이벤트별 상태 변경과 렌더링 업데이트를 담당하게 해볼 수 있어요.
과제 체크포인트
배포 링크
https://leehyunji0715.github.io/front_7th_chapter2-1/
기본과제
상품목록
상품 목록 로딩
상품 목록 조회
한 페이지에 보여질 상품 수 선택
상품 정렬 기능
무한 스크롤 페이지네이션
상품을 장바구니에 담기
상품 검색
카테고리 선택
카테고리 네비게이션
현재 상품 수 표시
장바구니
장바구니 모달
장바구니 수량 조절
장바구니 삭제
장바구니 선택 삭제
장바구니 전체 선택
장바구니 비우기
상품 상세
상품 클릭시 상세 페이지 이동
/product/{productId}형태로 변경된다상품 상세 페이지 기능
상품 상세 - 장바구니 담기
관련 상품 기능
상품 상세 페이지 내 네비게이션
사용자 피드백 시스템
토스트 메시지
심화과제
SPA 네비게이션 및 URL 관리
페이지 이동
상품 목록 - URL 쿼리 반영
상품 목록 - 새로고침 시 상태 유지
장바구니 - 새로고침 시 데이터 유지
상품 상세 - URL에 ID 반영
/product/{productId})상품 상세 - 새로고침시 유지
404 페이지
AI로 한 번 더 구현하기
과제 셀프회고
기술적 성장
자랑하고 싶은 코드
개선이 필요하다고 생각하는 코드
장바구니화면 처리, Detail Page랑 HomePage는 PageLayout이라는 공통 레이아웃을 쓰는데, 딱 장바구니 버튼이 PageLayout에 있다. 장바구니에 물건을 담거나, 장바구니 모달에서 변경이 있을 때마다 현재 HomePage를 render()를 호출한다. 이는 화면이 계속 깜빡거리는 이슈가 생겼다. 왜냐면 render를 하면 loader 가 무조건 실행되게 되어서... 그래서 이 부분은 그냥 명령형으로 일일이 구현해서 장바구니 처리는 render를 거치지 않고 실행되게 만들었다.나중에 정답 코드 보고 내 코드랑 비교해봐야겠다학습 효과 분석
라우터: 라우터의 개념을 좀더 이해할 수 있었습니다! => react-router는 실제로 어떻게 구현이 되어있을까? 궁금증이 생기더라고요!리액트: 리액트 컴포넌트에 대해 좀 더 이해할 수 있었습니다! 특히 컴포넌트를 js로 만들면서, 실제 제가 쓰는 React 랑 모양이 비슷해지는 것을 보고 내심 놀랐습니다! 리액트가 왜 렌더할때마다 컴포넌트 전체를 부른다 라는 의미가 와닿는 순간이었습니다. (물론 diff나 좀 더 최적화는 필요하겠지만, 그래도 뿌듯했어요)브라우저 API: js 로 구현하는 시간을 가지니, 좀더 브라우저 api와 친숙해졌습니다!AI: 성호 코치님 멘토링 시간 때 AI를 부사수처럼 써라! 라는 말씀을 깊이 새겼습니다. 설계는 제가 하고, 그 일을 ai에게 시키시라는 말씀을 명심했습니다. 그래서 조금 제가 원하는 구조에 대해 먼저 생각해보고, 필요한 기능을 ai에게 말한 후에, 코드를 짜달라고 했습니다! 그리고 AI가 짠 코드를 보며, 잘 작성했는지, 오류는 없는지 체크/수정해가며 구현했습니다. AI를 쓰는 태도를 배울 수 있어서 좋았습니다.과제 피드백
AI 활용 경험 공유하기
리뷰 받고 싶은 내용